Skip to content

planner: fix partial index wrongly kept due to shared Range pointer mutation - #70002

Open
yy782 wants to merge 4 commits into
pingcap:masterfrom
yy782:fix/union-ranges-avoid-mutating-input
Open

planner: fix partial index wrongly kept due to shared Range pointer mutation#70002
yy782 wants to merge 4 commits into
pingcap:masterfrom
yy782:fix/union-ranges-avoid-mutating-input

Conversation

@yy782

@yy782 yy782 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #69779

Problem Summary:

What changed and how does it work?

When query condition is a >= 0 and partial index condition is a < 3, CheckPartialIndexes wrongly keeps/retains the index (instead of pruning it), leading to incorrect query results. The root cause is UnionRanges mutating shared Range pointers, corrupting the caller's input and causing the implication check to fail. See issue #69779 for details.

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test
    • I checked and no code files have been changed.

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

Please refer to Release Notes Language Style Guide to write a quality release note.

None

Summary by CodeRabbit

  • Bug Fixes
    • Updated range unioning/merging to avoid mutating caller-provided ranges, preserving correct boundary metadata.
    • Improved planner behavior for partial indexes so the partial index is not selected when the query predicate doesn’t imply the index predicate.
  • Tests
    • Added a regression test to confirm range unioning preserves original inputs.
    • Added integration coverage for partial-index implication, including matching results when the partial index is ignored.

yy782 added 2 commits July 23, 2026 14:48
…ointer

UnionRanges builds sortRange objects holding pointers to the original
Range. When merging adjacent or overlapping ranges, it mutates the
pointed Range directly, corrupting the caller's input because the
pointer is shared across all sortRange items.

This causes CheckPartialIndexes to incorrectly determine that partial
index conditions are implied by query conditions, leading to wrong
index pruning.

Ref pingcap#69779
…p#69779)

Verify that a >= 0 does NOT imply a < 3 for partial index pruning,
which is the regression case from the shared pointer bug in UnionRanges.
@ti-chi-bot ti-chi-bot Bot added release-note-none Denotes a PR that doesn't merit a release note. do-not-merge/needs-triage-completed labels Jul 23, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign jackysp for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added contribution This PR is from a community contributor. size/M Denotes a PR that changes 30-99 lines, ignoring generated files. needs-ok-to-test Indicates a PR created by contributors and need ORG member send '/ok-to-test' to start testing. labels Jul 23, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

Hi @yy782. Thanks for your PR.

I'm waiting for a pingcap member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71783dcd-2a02-4065-939e-77c9a57b2bac

📥 Commits

Reviewing files that changed from the base of the PR and between 5aac63a and bbc574d.

📒 Files selected for processing (2)
  • pkg/util/ranger/ranger.go
  • pkg/util/ranger/ranger_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/util/ranger/ranger.go

📝 Walkthrough

Walkthrough

UnionRanges now protects caller-owned range metadata while merging. Partial-index integration coverage adds a predicate that does not imply the index condition and compares query results with and without the partial index.

Changes

Range isolation and planner validation

Layer / File(s) Summary
Preserve caller range metadata
pkg/util/ranger/ranger.go, pkg/util/ranger/ranger_test.go
UnionRanges lazily clones metadata before merge updates, resets detachment between ranges, documents the behavior, and tests unchanged inputs.
Validate partial-index implication
tests/integrationtest/t/planner/core/casetest/index/partialindex.test, tests/integrationtest/r/planner/core/casetest/index/partialindex.result
Adds a partial index on a < 3, queries using a >= 0, an IGNORE INDEX comparison, and recorded results.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: d3hunter

Poem

I’m a rabbit guarding ranges bright,
Cloning each datum snug and right.
Partial indexes hop in line,
Queries test each boundary sign.
No shared pointers nibble the hay!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: preventing partial-index bugs caused by shared Range mutation.
Description check ✅ Passed The description includes the issue number, problem summary, fix overview, and tests, so it is mostly complete.
Linked Issues check ✅ Passed The patch addresses issue #69779 by making UnionRanges non-mutating and adding tests for the unsafe partial-index case.
Out of Scope Changes check ✅ Passed The changes stay focused on the bug fix and related tests, with no clear unrelated or out-of-scope edits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@terry1purcell

Copy link
Copy Markdown
Contributor

/ok-to-test

@ti-chi-bot ti-chi-bot Bot added ok-to-test Indicates a PR is ready to be tested. and removed needs-ok-to-test Indicates a PR created by contributors and need ORG member send '/ok-to-test' to start testing. labels Jul 23, 2026
@terry1purcell
terry1purcell requested a review from Copilot July 23, 2026 15:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Not ready to approve

The UnionRanges merge path still assigns HighVal by reference (slice aliasing), which can reintroduce shared-state behavior and undermine the intended isolation fix.

Pull request overview

This PR addresses a planner correctness bug where partial indexes could be incorrectly accepted/pruned due to UnionRanges mutating shared *Range inputs, leading to wrong implication checks and potentially incorrect query results (ref #69779).

Changes:

  • Prevents UnionRanges from mutating caller-owned *Range instances by cloning the active merge range.
  • Adds an integration test case that reproduces and guards against the “partial index chosen when predicate doesn’t imply index predicate” wrong-result scenario.
  • Updates the corresponding integration test result file.
File summaries
File Description
pkg/util/ranger/ranger.go Clones the working range in UnionRanges to avoid mutating shared *Range pointers during merges.
tests/integrationtest/t/planner/core/casetest/index/partialindex.test Adds a regression query case ensuring a >= 0 does not incorrectly allow using a partial index defined with a < 3.
tests/integrationtest/r/planner/core/casetest/index/partialindex.result Records expected results for the new regression case (ensuring full rowset is returned).

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/util/ranger/ranger.go Outdated
Comment on lines +699 to +701
lastRange.encodedStart = objects[i].encodedStart
lastRange.encodedEnd = objects[i].encodedEnd
lastRange.originalValue = objects[i].originalValue.Clone()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After Clone(), lastRange.originalValue is already an independent Range object. The HighVal assignment on line 694 only changes where this cloned object's HighVal field points — it does not mutate the caller's original Range in any way.

While the cloned HighVal and objects[i].HighVal do share the same underlying array, the returned ranges' HighVal is only ever read (for range endpoint comparison), never mutated in-place by downstream consumers. So this slice aliasing is safe in practice.

I'm leaning toward keeping the current approach — it avoids an unnecessary copy and the semantics are correct for this use case. That said, if you feel a deep copy here adds more confidence, I'm open to it.

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.5857%. Comparing base (e35ad93) to head (bbc574d).
⚠️ Report is 14 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #70002        +/-   ##
================================================
- Coverage   76.3239%   73.5857%   -2.7382%     
================================================
  Files          2041       2060        +19     
  Lines        559772     583148     +23376     
================================================
+ Hits         427240     429114      +1874     
- Misses       131631     153479     +21848     
+ Partials        901        555       -346     
Flag Coverage Δ
integration 40.9049% <100.0000%> (+1.1996%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
dumpling 60.4471% <ø> (ø)
parser ∅ <ø> (∅)
br 45.1351% <ø> (-17.5863%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@yy782

yy782 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

/retest

@yy782

yy782 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

/cc @D3Hunter, @hawkingrei

@terry1purcell

Copy link
Copy Markdown
Contributor

I verified the fix end-to-end: the repro from #69779 returns wrong results (3 rows instead of 5) on master, and passes with your patch. Your root-cause diagnosis is correct: UnionRanges mutates the caller's *Range in place, so implCompareExpr ends up comparing the pre-predicate range against itself (the ran == other fast path in Range.Equal) and always accepts the implication when a merge occurs.

A few requested changes:

  1. Performance: cloning every output range penalizes hot paths — notably buildKvRangesForIndexJoin (runs per index-join lookup batch, up to ~25k ranges that are already private clones) and large IN-lists/DNFs. Please switch to copy-on-write: keep lastRange := objects[i] and clone only on the first actual mutation of a group leader in the merge branch. This is semantically equivalent (when nothing is mutated, returning the original pointer is honest) and keeps the common no-merge path allocation-free.
  2. Document the contract: add a note to UnionRanges' doc comment that it must not modify input ranges (ref this issue), otherwise a future cleanup could reintroduce the bug. (Related: the pre-clone at detacher.go:432-435 exists only because of this mutation — could be simplified in a follow-up.)
  3. Test: add explain format='brief' for the query in partialindex.test so the test pins that pi is rejected, rather than relying on the cost model choosing it. A small unit test in pkg/util/ranger asserting inputs are unchanged after UnionRanges would also lock the contract directly.
  4. PR description: the summary is inverted — the index condition is a < 3 and the query is a >= 0, and the bug is that the index is wrongly kept (wrong results), not wrongly pruned. Please fix the title/body accordingly, and use close Partial index implication check can choose an unsafe access path and return wrong results #69779 instead of ref since this fully fixes the repro.

@terry1purcell

Copy link
Copy Markdown
Contributor

/hold

@ti-chi-bot ti-chi-bot Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 27, 2026
UnionRanges reused the input slice's backing array and modified Range
objects in-place during merging, causing partial index pruning to
retain an unsafe access path and return wrong results.

Fix by using a copy-on-write strategy: clone a Range only when a merge
actually modifies it. Updated the doc comment and added a unit test
that verifies the non-mutation contract.

Close pingcap#69779
@ti-chi-bot ti-chi-bot Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Jul 27, 2026
@yy782 yy782 changed the title planner: fix partial index pruning due to shared Range pointer mutation planner: fix partial index wrongly kept due to shared Range pointer mutation Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
pkg/util/ranger/ranger.go (1)

691-698: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Clone only when the merged endpoint changes.

Line 691 clones for contained/duplicate ranges even though lines 695-698 make no mutation. Move the copy-on-write block inside the endpoint-extension branch to preserve the requested allocation avoidance for large union workloads.

Proposed fix
-			if !detached {
-				lastRange.originalValue = lastRange.originalValue.Clone()
-				detached = true
-			}
 			if bytes.Compare(lastRange.encodedEnd, objects[i].encodedEnd) < 0 {
+				if !detached {
+					lastRange.originalValue = lastRange.originalValue.Clone()
+					detached = true
+				}
 				lastRange.encodedEnd = objects[i].encodedEnd
 				lastRange.originalValue.HighVal = objects[i].originalValue.HighVal
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/util/ranger/ranger.go` around lines 691 - 698, In the range-merging
logic, move the copy-on-write block that clones lastRange.originalValue inside
the encodedEnd extension condition. Ensure cloning and setting detached occur
only when bytes.Compare(lastRange.encodedEnd, objects[i].encodedEnd) < 0 and the
merged endpoint fields are updated, while contained or duplicate ranges avoid
allocation.
pkg/util/ranger/ranger_test.go (1)

2690-2709: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover a second merge group after detachment resets.

This case ends immediately after the non-merge transition, so removing line 703 would still pass. Add a disjoint overlapping pair (for example [6,8], [7,9]) and assert both groups leave their original ranges unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/util/ranger/ranger_test.go` around lines 2690 - 2709, Extend the
UnionRanges test around the existing r1/r2/r3 setup with a second disjoint
overlapping pair, such as [6,8] and [7,9], so merging continues after the
detachment reset. Assert the merged output contains both groups and verify every
original range in both groups retains its bounds and exclusion flags unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/util/ranger/ranger_test.go`:
- Around line 2690-2709: Extend the UnionRanges test around the existing
r1/r2/r3 setup with a second disjoint overlapping pair, such as [6,8] and [7,9],
so merging continues after the detachment reset. Assert the merged output
contains both groups and verify every original range in both groups retains its
bounds and exclusion flags unchanged.

In `@pkg/util/ranger/ranger.go`:
- Around line 691-698: In the range-merging logic, move the copy-on-write block
that clones lastRange.originalValue inside the encodedEnd extension condition.
Ensure cloning and setting detached occur only when
bytes.Compare(lastRange.encodedEnd, objects[i].encodedEnd) < 0 and the merged
endpoint fields are updated, while contained or duplicate ranges avoid
allocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a1703511-6cf1-43d0-a6c3-bf4692b9f891

📥 Commits

Reviewing files that changed from the base of the PR and between c1c14ed and 5aac63a.

📒 Files selected for processing (4)
  • pkg/util/ranger/ranger.go
  • pkg/util/ranger/ranger_test.go
  • tests/integrationtest/r/planner/core/casetest/index/partialindex.result
  • tests/integrationtest/t/planner/core/casetest/index/partialindex.test
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/integrationtest/t/planner/core/casetest/index/partialindex.test
  • tests/integrationtest/r/planner/core/casetest/index/partialindex.result

Only clone a Range when the merge truly extends it (encodedEnd <
newEnd), avoiding unnecessary cloning when merging identical ranges.
Add a second overlapping group to the unit test to cover the "end a
merge group, start a new one" code path.

Close pingcap#69779
@yy782

yy782 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

A subtle point: UnionRanges reuses the backing array of the input slice (using ranges[:0] + append), meaning the caller's slice elements will be overwritten with new pointers. The only guarantee is that the original Range objects (r1, r2, r3) are not mutated. The doc comment currently says "does not modify the caller's ranges" — I wonder if that's sufficiently clear, or should we add an inline comment explaining the slice reuse? Alternatively, are there other options we should consider?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution This PR is from a community contributor. do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. ok-to-test Indicates a PR is ready to be tested. release-note-none Denotes a PR that doesn't merit a release note. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Partial index implication check can choose an unsafe access path and return wrong results

3 participants